詮釋資料是添加在符號或群集中的映射,其中記載了該符號或群集的資訊。使用 with-meta
函式添加詮釋資料,它將返回添加了資料的物件;或用 meta
函式取得詮釋資料:
(with-meta [1 2 3] {:trivial true})
;; => [1 2 3]
(meta (with-meta [1 2 3] {:trivial true}))
;; => {:trivial true}
或使用更簡便的方式,在映射前面加上插入符號 (^) 添加詮釋資料:
(def user ^{:birth "12-21"} {:name "Catherine"})
user
;; => {:name "Catherine"}
(meta user)
;; => {:birth "12-21"}
如果詮釋資料的映射中只有一個索引鍵與值的對應,而且值的內容爲真,則可以如以下的簡寫:
(def ^{:private true} x [1 2 3])
(def ^:private y [1 2 3])
以上的兩個符號都添加了私有的資訊,在其他的命名空間中無法取用。
若是用 def
或 defn
定義符號與 Var 物件時,在符號前面寫下詮釋資料,則詮釋資料將會被用在 Var 物件而不是符號,所以查看函式的詮釋資料必須查看儲存函式的 Var 物件,而不是符號:
(def ^{:doc "Nothing special"} x [1 2 3])
(meta x)
;; => nil
(meta (var x))
;; => {:doc "Nothing special", :line 1, :column 1, :file "/private/var/folders/5n/sm_s13cn3lb_p_4n2khqd0mr0000gn/T/form-init6817815229097680482.clj", :name x, :ns #namespace[user]}
函式的說明文件也是利用詮釋資料的方式,添加到儲存函式的 Var 物件上。Var 物件的 :doc
索引鍵對應的值便是該物件的說明文件:
(defn doublex "Double the param" [x] (* x x))
(meta #'doublex)
;; => {:arglists ([x]), :doc "Double the param", :line 1, :column 1, :file "/private/var/folders/5n/sm_s13cn3lb_p_4n2khqd0mr0000gn/T/form-init6817815229097680482.clj", :name doublex, :ns #namespace[user]}
從以上的範例可以看到,我們使用 meta
取得儲存函式 doublex
的 Var 物件的詮釋資料,其中的索引鍵 doc
便存放著定義函式時寫下的說明文件。
Clojure 的核心函式也攜帶了豐富的詮釋資料,其中有該函式的命名空間、Var 物件的名稱、參數列表、說明文件、該函式何時加入 Clojure 等等的資訊。以下是 str
函式的詮釋資料:
(meta #'str)
;; => {:added "1.0", :ns #namespace[clojure.core], :name str, :file "clojure/core.clj", :static true, :column 1, :line 533, :tag java.lang.String, :arglists ([] [x] [x & ys]), :doc "With no args, returns the empty string. With one arg x, returns\n x.toString(). (str nil) returns the empty string. With more than\n one arg, returns the concatenation of the str values of the args."}
讀取器將一般文字轉換成一連串的形式之後,交給編譯器 (Compiler) 編譯成 Java 虛擬機位元碼,其中有一些形式的求值方法不同於一般的形式,稱爲特殊形式 (Special forms)。
舉例來說,前面章節提到過的 if
形式是一種特殊形式,它不像一般形式會在呼叫之前,先將各個參數求值,而是依據條件式的真與假,才決定對哪一個分支繼續求值。
特殊形式是 Clojure 程式語言的基石,所有的東西都是藉由特殊形式而打造出來。以下介紹各種特殊形式:
def
根據給予的符號名稱與資料,建立全域的 Var 物件。
(def a 10)
對 if
的第一個參數求值,若爲真則求值第二個運算式,否而且有第三個運算式則求值。
(if (= a 10) "true" "false")
;; => "true"
依序對 do
其後的各個運算式求值,並返回最後一個運算式求值的結果。
(do
(println "Do")
(str "Something:" 42)
"else")
;; => Do
;; => "else"
以第一個參數向量中的符號與資料建立區域繫結,並對之後的運算式求值,區域繫結只在這些運算式有效。
(let [x 1
y 2]
y)
;; => 2
不對其後的形式求值,原封不動地返回。
(quote (a 1 2))
;; => (a 1 2)
Clojure 不會試圖去尋找以 a
爲名的函式並以參數呼叫,而是照實地返回。
fn
建立函式,函式名稱是否提供都是可選的,之後是以類似 let
的向量參數繫結,參數之後則是函式的本體。
Clojure 的函式實作了 Java 中的 Callable
、Runnable
與 Comparator
三種介面。
(def triplex
(fn this [x]
(* x x x)))
(triplex 3)
;; => 27
與 let
一樣,差別在於建立了遞迴點 (Recursion point) 與 recur
搭配使用,用來反覆循環其中的運算式。
對跟隨在 recur
之後的各參數求值,以新值返回遞迴點重新執行。遞迴點可以藉由 loop
或 fn
建立。
你可以把 loop/recur
視爲顯式 (Explicit) 的尾遞迴 (Tail recursion)。
(def fib
(fn [x]
(loop [a 0 b 1 cnt x]
(if (= cnt 0)
a
(recur (+' a b) a (dec cnt))))))
(fib 10)
;; => 55
求值其後的運算式,並將得到的例外拋出。
(throw (Exception. "my exception message"))
;; => Exception my exception message
try
有三個參數,第一個參數爲運算式本體,先對此運算式求值之後,若拋出例外且符合 catch
欲捕捉的例外,則執行 catch
的本體運算式。而不管是否有例外發生,finally
的本體運算式都會被求值。
經由本篇文章,你了解了什麼是讀取器以及讀取巨集,還有讀取巨集中各個字符代表的特殊功能;還知道了詮釋資料的用途,比如添加或取得詮釋資料。更了解了奠定 Clojure 基礎的各種特殊形式。
還不賴吧?今天就先到這裡,下一篇文章再見囉!
(本篇文章同步刊登於 GitHub,歡迎在文章下方留言或發送 PR 給予建議與指教)